﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Serialization;
using Common.Utility;
using DataInterpolator.Model;
using DataInterpolator.ViewModel.Messages;
using GalaSoft.MvvmLight.Messaging;
using OxyPlot;
using OxyPlot.Axes;

namespace DataInterpolator.ViewModel
{
   public class InterpHandlesViewModel
   {
      private bool _IsRefreshing = false;
      private int _UndoPosition = 0;
      private CircularBuffer<string> _UndoStack = new CircularBuffer<string>(100);
      private LinkedList<InterpPair> _InterpPairs = new LinkedList<InterpPair>();
      private List<InterpPairVMO> _InterpPairsVM = new List<InterpPairVMO>();

      private LinearAxis _TopMiddle = null;

      public InterpHandlesViewModel(OxyPlot.PlotModel plotModel)
      {
         LeftBounds = new Tuple<int, int>(0, 100);
         RightBounds = new Tuple<int, int>(0, 100);
         PlotViewModel = plotModel;

         foreach (var axis in PlotViewModel.Axes)
         {
            if (axis.Key == InterpolationViewModel.__MiddleKey)
            {
               _TopMiddle = (LinearAxis)axis;
               break;
            }
         }

         PlotViewModel.MouseDown += (s, e) =>
         {
            if (e.ChangedButton == OxyMouseButton.Left && e.ClickCount > 1)
            {
               double xPos = _TopMiddle.InverseTransform(e.Position.X);

               double xScale = (xPos - _TopMiddle.Minimum) / (_TopMiddle.Maximum - _TopMiddle.Minimum);

               if (xScale > 0.0 && xScale < 1.0)
               {
                  double y = PlotViewModel.DefaultYAxis.InverseTransform(e.Position.Y);

                  //Convert that back to index
                  int yIdx = ConvertToIdx(y);

                  var newPair = new InterpPair();
                  newPair.Left = yIdx;
                  newPair.Right = yIdx;

                  if (_InterpPairs.Count == 0)
                  {
                     _InterpPairs.AddLast(newPair);
                  }
                  else
                  {
                     var curr = _InterpPairs.First;

                     //Now we need to choose where to put this
                     while (curr != null)
                     {
                        if (curr.Value.Left * (1.0 - xScale) + curr.Value.Right * xScale > yIdx)
                           break;

                        curr = curr.Next;
                     }

                     if (curr != null)
                     {
                        newPair.Left = Math.Min(curr.Value.Left, yIdx);
                        newPair.Right = Math.Min(curr.Value.Right, yIdx);
                        _InterpPairs.AddBefore(curr, newPair);
                     }
                     else
                     {
                        newPair.Left = Math.Max(_InterpPairs.Last.Value.Left, yIdx);
                        newPair.Right = Math.Max(_InterpPairs.Last.Value.Right, yIdx);
                        _InterpPairs.AddLast(newPair);
                     }
                  }

                  Add(newPair);
               }
            }
         };

         Messenger.Default.Register<MenuCommandMessage>(this, OnMenuCommand);
      }

      private void OnMenuCommand(MenuCommandMessage command)
      {
         if (command.CommandType == MenuCommandTypes.Undo)
         {
            Undo();
         }
         else if (command.CommandType == MenuCommandTypes.Redo)
         {
            Redo();
         }
      }

      public PlotModel PlotViewModel
      {
         get; private set;
      }

      public double XOffset
      {
         get; set;
      }

      public double XScale
      {
         get; set;
      }

      public Tuple<int, int> LeftBounds
      {
         get; set;
      }

      public Tuple<int, int> RightBounds
      {
         get; set;
      }


      public void Add(InterpPair interpPair)
      {
         var pairVM = new InterpPairVMO(PlotViewModel, interpPair);

         pairVM.DoubleRightClick += () =>
         {
            pairVM.ValueChanged -= PairVM_ValueChanged;
            pairVM.StateChanged -= PairVM_StateChanged;

            if (_InterpPairsVM.Contains(pairVM))
            {
               pairVM.RemoveFromPlot();
               _InterpPairsVM.Remove(pairVM);
               _InterpPairs.Remove(pairVM.Pair);
            }

            RefreshInterpVM(null);
         };

         pairVM.ValueChanged += PairVM_ValueChanged;
         pairVM.StateChanged += PairVM_StateChanged;

         _InterpPairsVM.Add(pairVM);

         RefreshInterpVM(pairVM);

         if (!_IsRefreshing)
         {
            UpdateUndoList();
         }
      }

      private void PairVM_StateChanged(InterpPairVMO vm, InterpPairVMO.ActiveStates arg2)
      {
         if (vm.State == InterpPairVMO.ActiveStates.None)
         {
            UpdateUndoList();
         }

         RefreshInterpVM(vm);
      }

      private void PairVM_ValueChanged(InterpPairVMO vm, double dy)
      {
         //Check if dy is big enough to cause a change
         int dyIdx = (int)Math.Round(dy / XScale);

         int finalLeft = vm.Pair.Left;
         int finalRight = vm.Pair.Right;

         //Try to calc the desired
         if ((vm.State & InterpPairVMO.ActiveStates.Left) > 0)
         {
            finalLeft += dyIdx;
         }

         if ((vm.State & InterpPairVMO.ActiveStates.Right) > 0)
         {
            finalRight += dyIdx;
         }

         //Lastly, check if we have room
         var link = _InterpPairs.Find(vm.Pair);

         if (link.Previous != null)
         {
            finalLeft = Math.Max(finalLeft, link.Previous.Value.Left);
            finalRight = Math.Max(finalRight, link.Previous.Value.Right);
         }

         if (link.Next != null)
         {
            finalLeft = Math.Min(finalLeft, link.Next.Value.Left);
            finalRight = Math.Min(finalRight, link.Next.Value.Right);
         }

         vm.Pair.Left = Math.Min(Math.Max(LeftBounds.Item1, finalLeft), LeftBounds.Item2);
         vm.Pair.Right = Math.Min(Math.Max(RightBounds.Item1, finalRight), RightBounds.Item2);

         RefreshInterpVM(vm);
      }

      public void Remove()
      {

      }

      public void Clear()
      {
         _InterpPairs.Clear();

         FullRefresh();

         if (!_IsRefreshing)
         {
            UpdateUndoList();
         }
      }

      public void Undo()
      {
         if (_UndoPosition + 1 >= _UndoStack.Count)
            return;

         LoadUndoPosition(++_UndoPosition);
      }

      public void Redo()
      {
         //If we are at the top of the stack, nothing to redo
         if (_UndoPosition == 0)
            return;

         LoadUndoPosition(--_UndoPosition);
      }

      private int ConvertToIdx(double xVal)
      {
         xVal -= XOffset;
         xVal /= XScale;

         return (int)Math.Round(xVal);
      }

      private double ConvertFromIdx(int xIdx)
      {
         return xIdx * XScale + XOffset;
      }

      private void LoadUndoPosition(int position)
      {
         if (position >= _UndoStack.Count)
            return;

         XmlSerializer serializer = new XmlSerializer(typeof(List<InterpPair>));

         using (TextReader ms = new StringReader(_UndoStack[position]))
         {
            List<InterpPair> list = (List<InterpPair>)serializer.Deserialize(ms);

            _InterpPairs = new LinkedList<InterpPair>(list);

            FullRefresh();
         }
      }

      private void UpdateUndoList()
      {
         //Serialize the current values
         XmlSerializer serializer = new XmlSerializer(typeof(List<InterpPair>));

         using (StringWriter ms = new StringWriter())
         {
            serializer.Serialize(ms, GetInterpList());

            string data = ms.ToString();

            while (_UndoPosition > 0)
            {
               if (_UndoStack.Count == 0)
               {
                  _UndoPosition = 0;
                  break;
               }

               _UndoStack.RemoveAt(0);
               _UndoPosition--;
            }

            _UndoStack.Enqueue(data);
         }
      }

      public void FullRefresh()
      {
         _IsRefreshing = true;

         //Start by deleting all of the current ones
         foreach (var vm in _InterpPairsVM)
         {
            vm.RemoveFromPlot();
         }

         _InterpPairsVM.Clear();

         //Now add a new VM object for each pair
         foreach (var pair in _InterpPairs)
         {
            Add(pair);
         }

         _IsRefreshing = false;

         //Null will refresh all
         RefreshInterpVM(null);
      }

      public List<InterpPair> GetInterpList()
      {
         return new List<InterpPair>(_InterpPairs);
      }

      private void RefreshInterpVM(InterpPairVMO interpVM)
      {
         if (_IsRefreshing)
            return;

         LinkedListNode<InterpPair> curr = _InterpPairs.First;

         //First verify all values are valid. Forward pass
         while (curr != null)
         {
            if (curr.Previous != null)
            {
               curr.Value.Left = Math.Max(curr.Previous.Value.Left, curr.Value.Left);
               curr.Value.Right = Math.Max(curr.Previous.Value.Right, curr.Value.Right);
            }
            else
            {
               curr.Value.Left = Math.Max(LeftBounds.Item1, curr.Value.Left);
               curr.Value.Right = Math.Max(RightBounds.Item1, curr.Value.Right);
            }

            curr = curr.Next;
         }

         curr = _InterpPairs.Last;

         //Backwards pass
         while (curr != null)
         {
            if (curr.Next != null)
            {
               curr.Value.Left = Math.Min(curr.Next.Value.Left, curr.Value.Left);
               curr.Value.Right = Math.Min(curr.Next.Value.Right, curr.Value.Right);
            }
            else
            {
               curr.Value.Left = Math.Min(LeftBounds.Item2, curr.Value.Left);
               curr.Value.Right = Math.Min(RightBounds.Item2, curr.Value.Right);
            }

            curr = curr.Previous;
         }

         List<InterpPairVMO> vmList = new List<InterpPairVMO>();

         if (interpVM == null)
         {
            vmList.AddRange(_InterpPairsVM);
         }
         else
         {
            vmList.Add(interpVM);
         }

         foreach (var vm in vmList)
         {
            vm.Refresh(XOffset, XScale);
         }

         PlotViewModel.InvalidatePlot(true);
      }
   }
}
